package com.ifonly.activerecord;

import com.ifonly.activerecord.dialect.Dialect;
import com.ifonly.activerecord.exceptions.ActiveRecordException;
import com.ifonly.activerecord.helpers.TableMapHelper;
import com.ifonly.activerecord.providers.DataSourceProvider;
import com.ifonly.activerecord.proxy.ConnectionSqlReportHandler;
import com.ifonly.activerecord.proxy.ProxyFactory;
import com.ifonly.activerecord.sql.config.Sql;
import com.ifonly.config.Configuration;
import com.ifonly.logger.Logger;
import com.ifonly.utils.ClassUtils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author <a href="mailto:ifonlymaster@163.com">ifonly</a>
 * @version 1.0 2015-12-17 16:03
 * @since JDK 1.6
 */
public final class Db {

    // ------------------------------------------ single instance

    private Db() {
    }

    private static class InstanceHolder {
        private static Db db = new Db();
    }

    public static Db getInstance() {
        return InstanceHolder.db;
    }

    private Configuration configuration = Configuration.getInstance();
    private DataSource dataSource;
    private Logger logger;
    private Dialect dialect;

    private int transactionIsolation = Connection.TRANSACTION_READ_COMMITTED;

    // ------------------------------------------ init

    public void init(DataSourceProvider dataSourceProvider) {
        Sql.init();

        this.dataSource = dataSourceProvider.getDataSource();
        this.logger = Logger.getLogger(Db.class);
        this.dialect = Dialect.create(configuration.getDialect());
        this.transactionIsolation = configuration.getTransactionIsolation();
    }

    public Dialect getDialect() {
        return dialect;
    }

    public int getTransactionIsolation() {
        return transactionIsolation;
    }

    // ------------------------------------------ Connection

    // ThreadLocal to hold connection
    private ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<Connection>();

    /**
     * Get connection, first get from ThreadLocal, if null, get from dataSource
     * if dev.mode return the connection proxy
     *
     * @return Connection
     * @throws SQLException maybe throws SQLException
     */
    public synchronized Connection getConnection()  {
        Connection connection = connectionThreadLocal.get();
        if (connection != null) {
            return connection;
        }

        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

        if (configuration.isDevMode()) {
           connection = ProxyFactory.newProxyInstance(connection,
                   new ConnectionSqlReportHandler(connection));
        }
        return connection;
    }

    /**
     * Get Connection from ThreadLocal for current thread
     *
     * @return Connection
     */
    public Connection getThreadLocalConnection() {
        return connectionThreadLocal.get();
    }

    /**
     * set Connection to current thread
     *
     * @param connection Connection instance
     */
    public synchronized void setThreadLocalConnection(Connection connection) {
        if (connection == null) {
            throw new IllegalArgumentException("param connection is null");
        }
        connectionThreadLocal.set(connection);
    }

    /**
     * close and remove connection in threadLocal
     */
    public synchronized void closeAndRemoveThreadLocalConnection() {
        try {
            Connection connection = connectionThreadLocal.get();
            if (connection != null) {
                DbUtils.close(connection);
            }
            connectionThreadLocal.remove();
        } catch (SQLException e) {
            logger.error("getThreadLocalConnection failed", e);
            throw new ActiveRecordException("getThreadLocalConnection failed", e);
        }
    }

    /**
     * close connection
     *
     *  if threadLocal has no connection
     *
     * @param conn connection to close
     * @throws SQLException Exception to throws
     */
    public synchronized void closeConnection(Connection conn)  {
        if (connectionThreadLocal.get() == null) {
            try {
                DbUtils.close(conn);
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

    // ------------------------------------------ Table Mappers

    // map table name and model class
    private Map<String, Class<? extends Model>> tableMapping = new HashMap<String, Class<? extends Model>>();
    // Table Mapper
    private TableMapper tableMapper = new TableMapper(); //TableMapper.getInstance();
    // Model Mapper
    private ModelMapper modelMapper = new ModelMapper(); //TableMapper.getInstance();

    public TableMapper getTableMapper() {
        return tableMapper;
    }

    public ModelMapper getModelMapper() {
        return modelMapper;
    }

    public Table getTable(Class<? extends Model> clazz) {
        return tableMapper.getTable(clazz);
    }

    @SuppressWarnings("unchecked")
    public Db addTableMapper(String className) {
        Class<?> clazz = ClassUtils.loadClass(className);
        if (Model.class.isAssignableFrom(clazz)) {
            return addTableMapper((Class<? extends Model>) clazz);
        } else {
            throw new ActiveRecordException("the class[" + className + "] not a subclass of Model");
        }
    }

    public Db addTableMapper(Class<? extends Model> clazz) {
        String tableName = TableMapHelper.tableName(clazz);
        return addTableMapper(tableName, clazz);
    }

    public Db addTableMapper(String tableName, Class<? extends Model> clazz) {
        tableMapping.put(tableName, clazz);
        return this;
    }

    /**
     * build mapping
     */
    public void buildTableModelMappers() {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            TableBuilder builder = new TableBuilder(conn);
            Set<String> keySet = tableMapping.keySet();
            for (String key : keySet) { //The key is the tableName
                Class<? extends Model> model = tableMapping.get(key);
                String sql = dialect.buildTableSql(key);

                Table table = builder.build(key, model, sql);

                tableMapper.addTableMapper(model, table);
                modelMapper.addModelMapper(model);
            }
        } catch (SQLException e) {
            throw new ActiveRecordException("build table failed", e);
        } finally {
            try {
                DbUtils.close(conn);
            } catch (SQLException e) {
                /* ignore */
            }
        }
    }

    // ------------------------------------------------------------ utils

    public void close(PreparedStatement ps) throws SQLException {
        DbUtils.close(ps);
    }

    public void close(ResultSet rs) throws SQLException {
        DbUtils.close(rs);
    }

    public void close(ResultSet rs, PreparedStatement ps, Connection conn) throws SQLException {
        DbUtils.close(rs);
        DbUtils.close(ps);
        closeConnection(conn);
    }

}
